Skip to content

feat: factor refactor D5b — evaluation contract v1.0, diagnostics channel, 14:50 visibility guards (C4 halted, see body) - #99

Merged
StackOverFlow11 merged 6 commits into
mainfrom
feat/factor-refactor-d5b-eval-contract
Jul 25, 2026
Merged

feat: factor refactor D5b — evaluation contract v1.0, diagnostics channel, 14:50 visibility guards (C4 halted, see body)#99
StackOverFlow11 merged 6 commits into
mainfrom
feat/factor-refactor-d5b-eval-contract

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Factor-layer refactor step D5, part B: the evaluation contract v1.0 (C3), the diagnostics channel the unified runner will need, and the 14:50 intraday-visibility property tests. C4 was halted, C5/C6 were not started, and nothing was deleted — the halt is the most important thing in this PR and is explained below.

Why C4 stopped

Scoping C4 turned up a confirmed look-ahead defect plus three premise problems that decide the shape of the unified runner. Building half a runner on top of unresolved premises would have been worse than stopping, and truncating the factor to "make the run work" would have smuggled a correction to a published result into a refactor commit — the exact shape this project keeps catching. So: report, don't smuggle.

The look-ahead (confirmed independently, two parties, different symbols and windows)

jump_amount_corr_20 is the only one of the eleven minute factors that does not truncate at 14:50 (decision_time / prepare_visible_minute_bars references: 0, versus 4–10 for the other ten). The old runner reads [start, end 23:59:59] — whole-day bars — and hands them straight to compute_jump_amount_corr, with no truncation anywhere downstream.

Under exec_to_exec the day-d entry anchor is the VWAP of the 14:51 bar on day d, so a day-d value computed from 14:50–15:00 bars reaches past its own entry anchor. Perturbation tests (both parties assert up front that post-cutoff rows really changed and pre-cutoff rows are byte-identical):

party window jump_amount_corr the other factors
implementer (12 CSI500 symbols) 2021-07..12 1,477 / 1,477 cells move, max|diff| 1.276 9 bound factors: 0
reviewer (different 12, different window) 2023-03..08 1,373 / 1,373 move, max|diff| 1.054 all 11 others (incl. the two the implementer missed — valley_price_quantile_stats, mmp_ew): 0
reviewer, sharper cut: perturb only bar_end >= 14:51 1,363 / 1,363 move, max|diff| 1.304

That last row is the decisive one: the signal sees the very minute it trades in, and the nine after it.

Meanwhile the published artifact declares the check it did not perform — spec.decision_cutoff="14:50:00", spec.return_basis="exec_to_exec", execution_window="[14:51:00,14:56:59]" — and awards Watch with the predictive axis PASS. qt/exec_basis_eval.py's decision_cutoff comes from the execution parameters and has nothing to do with how the factor was computed.

Why no existing test caught it: test_pit_perturbing_future_bars_does_not_change_factor_at_d guards the cross-day boundary, and its fixture session is six bars at 09:31–09:36 — there is no 14:50 boundary in it at all. The intraday boundary only became load-bearing when PR #79 moved the entry anchor from close(d) to 14:51(d), and #79's review never returned; this falls inside its "not independently checked" list.

The correction is handled by a separate correctness-fix PR, not here: the 14:50 truncation is a standing research-side authorization (2026-07-19: "全日数据因子一律截断到 14:50…授权直接截断,不必逐因子再问") and this factor predates it, so truncating is applying an existing rule rather than making a new research decision — but from the refactor's point of view it still changes a published factor's values, and §〇 forbids the refactor changing definitions. Both things are true; the resolution is that it gets its own PR, labelled a correctness fix.

What this PR does contain

  1. 6046bbe C3 — evaluation contract v1.0: EvalConfig.view / return_basis with pairing legality enforced at construction via D0's require_legal_pairing (rule delegated, not restated); provenance gains requires / adjustment / overnight_boundary / lookback_depth plus a top-level eval_contract_version; analytics/eval/summary.py refuses to render a cross-basis summary that omits view/return_basis (declaring the columns but leaving them blank is refused too). The verdict gates were not touched: the reviewer confirmed verdict.py / sections.py / standard.py / ir.py / evaluator.py / report.py are md5-identical to main, and every default (min_abs_icir=0.30, min_incremental_abs_icir=0.15, min_monotonicity_spearman=0.0, min_rebalances=12, min_effective_samples=24.0, min_span_days=365, min_ic_win_rate=0.55, min_abs_nw_t=2.0) is pinned by value in tests.
  2. 0363f0c Diagnostics channel for the coverage disclosures the unified runner must carry: per_symbol_diagnostics is a separate callable from per_symbol, so the values path D4b verified cell-by-cell can never be perturbed by a diagnostics request (reviewer's mutation: multiply diagnosed values by 1+1e-9 → the isolation test catches it). Three of eleven factors carry diagnostics_out, and the store persists values only, so a disclosure request must recompute — that cost is written into behaviour and pinned (test_a_warm_store_serves_values_but_a_sink_forces_the_recompute), because the alternative is dropping a mandatory section, i.e. silent degradation.
  3. cc06d32 A defect the implementer found in its own work two commits earlier and fixed in its own commit: EvalConfig's identity default must be the legacy pairing, because the eleven runners each build one config and feed both the close and the exec side; defaulting to decision/exec would have made every close artifact declare a basis it was not scored on.
  4. c6c03af 14:50 visibility property tests + qt/panel_leg_probe.py (the tool that produced the panel-leg evidence above).
  5. babcc9b Review fixes — see below.

Review (adversarial): APPROVE-WITH-NITS, all findings closed

MEDIUM-1 — a guard that could not guard itself. The test claiming to pin cc06d32's fix was a substring match on function source (assert "replace(" in source and "EXEC_TO_EXEC" in source ...). The reviewer emptied the real assignment into a bare replace(eval_cfg) and left the two names in comments: every exec artifact would then inherit the caller's close identity — declaring view=close / close_to_close while being scored on exec returns — and 52 tests across all three relevant files passed. This is #78's lesson again (a lexical assertion cannot hold a semantic claim), failing in the direction that re-admits the very defect the same commit had just fixed. Fixed by extracting exec_identity(cfg, *, factor_id, book_view) and asserting behaviour. Reviewer re-ran the identical escape on the new code: 4 failed, rc=1, red at two levels; and separately verified the false-positive direction is gone (moving the same call into a helper — behaviour identical, literal names gone — stays green at 58 passed).

MEDIUM-2 — a false claim one level up. The exec path declared view=decision unconditionally, but that is untrue for with_book runs, whose factor book (value_ep / value_bp / volatility_20) is taken at close(d) plus that evening's daily_basic — the live defect §1.1 itself documents and defers to D7. The field is defined as "the information-set view the factor values were taken under", and a with-book run carries two information sets; one field silently reported the clean one. This is precisely the class of defect the field exists to prevent, one level up, and it was free to fix now because no artifact carries the field yet — once C5/C6 runs it would be baked into 22 of them.

Fixed by splitting the two facts rather than registering an exception (registering alone cannot make view=decision true for with_book): a new EvalConfig.book_view declared by the only informed party, and subject_view(factor_id) derived from measured fact — the deny list NOT_DECISION_CUTOFF_SAFE lives on the factor side (it is a fact about values, not about the evaluation path) and is measured by the visibility test rather than declared; the test's local copy was deleted, since two copies are two things to keep in sync. Reviewer's mutations: emptying the deny list → 3 failed, including the measurement test itself catching jump_amount_corr as dirty; reverting subject_view to a hardcoded constant → 1 failed.

Deliberately not validated: book_view × return_basis pairing. The reviewer's adjudication is worth recording as the general rule — a mismatch that destroys meaning must be blocked; a mismatch that only introduces a disclosed, sign-known bias must be recorded. A view × basis mismatch scores values from one information set against returns anchored in another: no honest artifact exists. A close-view book under exec returns is a control variable on the Incremental axis whose bias direction is pre-registered (§1.1/R24: the book is a mediator, so the current basis understates with_book incremental ICIR) — blocking it would not unmix the information sets, it would only stop the artifact from saying that they are mixed.

A deliberate loud block ships with this: jump_amount_corr_20 now derives (close, exec_to_exec), which EvalConfig refuses, so its exec evaluation raises. That is correct — until the truncation lands, no honest exec artifact for it exists, and blocking beats a false claim (red line #9). The message names the factor, the derived view, the mechanism, the measurement source, and the consequence, and chains the generic pairing error as __cause__. Removal is one deny-list entry, tracked by the correctness-fix PR.

The implementer then found the same shape one level below, in its own new tests, and reported it rather than quietly patching: a unit test on exec_identity cannot speak to how the caller wires it — passing book_view=None for the with-book run (book supplied, report says none) left 28 tests green. End-to-end assertions now read the two actually-written JSON files, check the rendered Markdown, and assert the caller's own config is not written back. Reviewer added a second mutation closer to a real regression (both runs sharing one config) — also red.

LOW-1 (a test named ..._is_authored_once that asserted formatting, not author-once) renamed; the property does hold.

Lead independent gates (re-run after the reviewer finished mutating, since a lead measuring a tree a reviewer is mutating is its own hazard): pytest 2227 passed, 1 skipped (2228 collected; main 2175 → +46 → 2221 → +7; 0 existing tests displaced) · ruff clean · phase0 anchor ic 0.9600 / annual 0.8408 · validate-config 31/31 · secret scan 0 · exec_baseline_freeze --verify 77/77 at 45c14aa · frozen panels untouched · worktree clean.

Registered for C5, before any artifact is regenerated

docs/factors/d5_runner_difference_catalogue.md §7b now records the drift this PR causes, so reconciliation does not chase it as a regression: eval_config +3 keys, one top-level key, four Markdown lines — all additive, so C5's byte-level comparison is known not to hold and value-level comparison plus structural assertions is what can. Two further entries: the exec side's no_book and with_book eval_config blocks now legitimately differ (null vs "close") and must not be read as a regression between them; and jump's exec evaluation now raises by design, with the release condition spelled out — otherwise someone will later see two artifacts missing and assume the run broke.

Open items this PR does not touch (each ruled on, each queued separately)

  • jump_amount_corr truncation + withdrawing/restating its published exec verdict — separate correctness-fix PR, in flight. Its priority is now effectively raised: with the loud block in place its exec evaluation cannot run at all, so C5/C6 would otherwise be missing those two artifacts.
  • Store has no universe dimension (design amendment A2) — pre-existing, CRITICAL for D6/D7, not introduced here. Two independent failure modes: _ensure_coverage computes gaps by date only, not (date, symbol), so a second batch finds "no gap" and its symbols vanish from the cross-section — affecting all eleven factors, and it is the very red line amendment A1 invoked to reject caller-side batching; and intraday_amp_cut's z-score happens at materialization, before the store, so stored values are a function of the universe while the key says otherwise (measured: fill with 12 symbols, read with 24 → 0 provider calls, 24 rows instead of 48, all 24 differing, max|Δ| 0.159–0.186). Queued as D4c, a prerequisite for C4. Adding a universe dimension to the key was rejected: it would over-invalidate the ten universe-independent factors, and a PIT universe is not a single set over a date range, so the dimension is not even definable — the store should hold per-symbol intermediates and combine cross-sectionally at read.
  • valley_price_quantile double lag — the proposed third path (shifted panel + the factor's internal T-1 disabled) was measured bit-identical (max|diff| = 0.000e+00, Series.equals True) with a breaking control at 7.3e-2, so it is an implementation change and is authorized — with two landing conditions: the equivalence test must pin both sides (it relies on both being per-symbol positional shifts on one date grid), and the shifted panel needs one extra day of history at the left edge.
  • 996 vs 995: the real evaluation universe is 996; the frozen minute panels are 995 (300114.SZ has no minute bars — the frozen panels themselves prove it: the three daily book-factor panels carry 996 including it, the ten minute panels carry 995 without it), and qt/saturation_probe.py's default universe derives from a 995 panel.

Two non-blocking NITs recorded: the block fires after the runner's expensive minute aggregation rather than at entry (self-resolving once the truncation lands and the deny-list entry goes); and IDENTITY_FIELDS does not yet include book_view, which will matter the first time a summary puts no_book and with_book side by side.

No CLAUDE.md/AGENTS.md changes here (docs PR follows). Frozen baselines untouched.

…ds + basis-column guard (D5 C3)

The adjudication core is upgraded, not rewritten. v0.9 could describe a factor and
one evaluation of it but could not say WHICH information set the values were taken
under — survivable while every run used one basis, not survivable since PR #79
started scoring the same factor on two, because two reports then carried the same
verdict word for two different statistical claims.

v1.0 adds exactly two identity fields and renders four declarations that were
already mandatory on the factor side:

* EvalConfig.view / EvalConfig.return_basis, validated as a LEGAL PAIRING at
  construction through data.availability_policy.require_legal_pairing (the D0
  single source — the rule is delegated, not restated). A close-view factor scored
  on exec_to_exec is now a readable error instead of a convention.
* the provenance box states requires / adjustment / overnight_boundary /
  lookback_depth as named rows rather than leaving them to vars(spec)'s reprs, and
  an EMPTY requires declaration is marked rather than rendered as a blank row.
* analytics/eval/summary.py: any cross-basis verdict table REFUSES to render
  without the identity columns, and refuses a declared-but-blank one too (R24).
  Filling a missing basis in would be the silent degradation the project forbids;
  guessing it from the majority of rows is worse than refusing.

WHAT DID NOT MOVE, and is now pinned by value rather than by prose: every
VerdictThresholds default (min_abs_icir=0.30, min_incremental_abs_icir=0.15,
min_monotonicity_spearman=0.0, the three-part sample gate). Design v3.2 §十 R24
forbids re-calibrating a bar inside the refactor; a threshold that moved here would
make every verdict in this cycle uninterpretable, and would do it silently.

The artifact drift this causes (2 keys in eval_config, 1 top-level key, 4 Markdown
provenance rows — all additions, no deletions, no metric change) is registered in
docs/factors/d5_runner_difference_catalogue.md §七之二 BEFORE the C5 reconciliation
runs, so it cannot later be chased as a D5 regression. That section also states the
consequence the plan did not: byte-for-byte equality of the 22 md/json artifacts is
NOT available as a C5 pass condition, and treating it as one would read this
upgrade as a regression.

Mutation evidence (run, not claimed):
* remove the _check_view_basis call -> 4 tests FAIL (rc=1), restored -> pass;
* narrow REQUIRED_SUMMARY_COLUMNS to ("view",) -> the return_basis-omitted case and
  the blank-cell case FAIL, restored -> pass.

Gates: pytest 2210 passed / ruff clean / 31 configs / phase0 ic 0.9600 annual 0.8408
unchanged / exec baseline --verify 77/77 at head 45c14aa / frozen panel mtimes
untouched.
…(D5 C4 groundwork)

FOUND WHILE SCOPING C4, and the reason this is a separate commit: four of the
eleven minute factors publish a scarcity / neutralization disclosure as an ADDED
report section, and three of them build it from a per-symbol DAY-LEVEL frame the
factor's compute function emits through its diagnostics_out sink. The old eval
runners had that frame because they ran the compute themselves. The unified runner
will not: values come from the D3 store through the D4/D4b materializer, and the
store persists VALUES ONLY. The day counts are not in it and cannot be recovered
from it.

That left exactly two fates for the disclosure — recompute it, or drop it. Dropping
a disclosure because the plumbing changed is the silent degradation this project
forbids, so the channel exists and its cost is stated wherever it is paid:

* MinuteStreamBinding gains an OPTIONAL per_symbol_diagnostics callable, kept
  SEPARATE from per_symbol on purpose — per_symbol is the value path D4b verified
  cell for cell, and a diagnostics request must not be able to change it. Asking
  re-runs the per-symbol math on bars already in memory: no extra I/O, no value
  effect. Bound for the three factors that publish a frame; the others answer
  has_minute_diagnostics() == False and return an EMPTY frame, which a caller must
  never read as "no attrition" (hence the separate, explicit predicate).
* materialize_range(..., diagnostics=sink) — default None is the D4b path byte for
  byte. The sink is EMIT-WINDOW scoped, so the denominator keeps the meaning it has
  in the frozen artifacts ("the days this run scored"), not "the days that were
  loaded"; for a pooled factor only the TERMINAL saturation load is observed, since
  the expansion steps are the search, not the computation. A daily factor plus a
  sink is a readable error rather than an empty sink.
* service.panel(..., diagnostics=sink) materializes the WHOLE requested range even
  on a full store hit, and refuses a shared sink for several factors (the frames
  carry no factor label, so a summarizer would silently reduce the mixture).

COST, stated rather than buried: three of eleven factors can never be served from a
warm store when their disclosure is wanted. That is a permanent charge the D5 plan
did not price, and it is the honest half of the trade above.

Mutation evidence (run, not claimed; each mutation asserted to hit its target):
* _sink_diagnostics returns an empty frame -> the VACUITY pre-assertion in the
  values-unchanged test FAILS (rc=1) instead of the equality passing for the wrong
  reason — the sink-stayed-empty shape is what makes such a test unfailable;
* drop the emit-window slice -> the scoping test FAILS with 576 loaded days;
* remove the sink-forces-refill branch -> the warm-store test FAILS on calls == 0.

Gates: pytest 2210 passed / ruff clean / 31 configs / phase0 ic 0.9600 annual 0.8408
unchanged / exec baseline --verify 77/77 / frozen panel mtimes untouched.
…ers that omit it

RECORDING MY OWN DEFECT, one commit old. 6046bbe defaulted EvalConfig.view /
return_basis to decision / exec_to_exec — the pairing the design is converging on,
and the wrong default for the code that exists today.

Each of the eleven live runners builds ONE EvalConfig (_build_eval_config) and hands
the SAME object to its close_to_close reports AND, through qt.exec_basis_eval, to its
exec ones. With the flattering default, every close artifact would have declared a
basis it was not scored on — the describe-the-check drift of #76/#78/#82, introduced
by the very field added to prevent it, in the same commit that added it. Nothing had
re-run yet, so no artifact carries the false label; the fix is before the fact.

A default's only job is to be TRUE for the callers that omit the field. Those callers
are on the close basis, so the default is close / close_to_close, and the exec path
states its identity EXPLICITLY — mirroring what qt.exec_basis_eval already does one
line earlier for the SPEC (intraday_spec_variant). When the close runners retire in
C6, flipping the default is a one-line change with no false statement left behind.

Pinned: a test asserts the default IS the legacy pairing and says why, and a
structural test asserts qt.exec_basis_eval restates the identity rather than
inheriting the caller's. The identity phrase test now covers both pairings, so the
author-once helper cannot be right for one and wrong for the other.
…corr as the one exception

FOUND BY MEASURING BEFORE COMMITTING TO THE MULTI-HOUR C5 RUN. qt/panel_leg_probe.py
compares the D4/D4b materializer's values on the real evaluation plane against the
D1 frozen panel — the fourth leg's comparison, taken as a probe first (the D5a
discipline that stopped D5 being started blind). The expectation was a small,
front-loaded warm-up difference. jump_amount_corr_20 came back with 1,155,500 of
1,158,382 both-finite cells differing, 931,394 of them AFTER year one where warm-up
cannot be the explanation, max relative difference 1.99 on a correlation bounded in
[-1, 1].

CHASED RATHER THAN EXPLAINED AWAY, and the cause is not the engine:

* reproducing the OLD runner's exact read + compute for one symbol matches the
  frozen panel EXACTLY (1210 cells, max |diff| 0.000e+00) — the baseline is
  reproducible and the comparison target is right;
* the same symbol under the new CLOSE view differs in 19 of 1210 cells (the warm-up
  difference that WAS expected); under the DECISION view, all 1210 differ;
* so the whole difference is the 14:50 truncation the decision view applies — and
  compute_jump_amount_corr is the ONLY one of the eleven with no decision-time
  truncation of its own (zero decision_time / prepare_visible_minute_bars references
  in its module; the other ten have 4-10 each), so the old runners handed it
  09:30-15:00 of day d.

THE DECISIVE TEST, now committed. Perturb ONLY the bars strictly after the cutoff,
assert every pre-cutoff row byte-identical, ask whether the day-d value moves. On
the real cache (12 CSI500 names, 2021-07..2021-12, 361,500 bars, 4.6% post-cutoff):
nine factors moved 0 cells at max |diff| 0.000e+00; jump_amount_corr_20 moved
1,477 of 1,477 at max |diff| 1.276. The committed test reproduces the same split on
synthetic bars with a realistic 09:31-11:30 / 13:01-15:00 session (a session ending
before 14:50 would make the perturbation a no-op — refused by a pre-assertion).

WHAT IT MEANS, stated and not fixed. Under exec_to_exec — the only basis the D5/D7
contract keeps — the entry anchor for date d is d's 14:51 execution bar, so a value
built from d's 14:50-15:00 bars uses information from after its own entry. PR #79's
merge-time self-check asserted the opposite in general ("the factor is known at
14:50"); for this one factor that is false, and #79's review never returned. Its
exec report also DECLARES decision_cutoff=14:50 (via intraday_spec_variant), which
its values do not honour. On close_to_close, entry is close(d) and using day d is
the conventional, already-declared-optimistic boundary, not a new defect.

Flagged, not repaired, per the D5a precedent for found defects: truncating it would
change a published factor's values and could move its verdict — a research decision
with a pre-registered hypothesis attached, not a refactor's business (§〇). The
exception is asserted POSITIVELY, so a later "fix" turns the test red and forces the
call to be made deliberately rather than noticed afterwards.

CONSEQUENCE FOR C5 the plan did not anticipate: for this factor the frozen D1 panel
and the new decision-view engine differ wholesale, and the correct engine is the NEW
one. The fourth leg's "relative <= 1e-12" cannot be the pass condition here; the
attribution is "the baseline had no 14:50 truncation for this factor", and making
the new engine reproduce the baseline would be reproducing the leak.
…o runs, two claims

Review APPROVE-WITH-NITS follow-up: MEDIUM-1, MEDIUM-2, LOW-1.

MEDIUM-1 — the test that claimed to pin cc06d32 could not.
It matched SUBSTRINGS in the function's source
(`"replace(" in source and "EXEC_TO_EXEC" in source and "DECISION" in source`).
Review broke it by hand: gut the assignment to a bare `replace(eval_cfg)`, leave the
two tokens in a COMMENT — the exec path then inherits the caller's CLOSE identity and
every exec artifact declares `view=close/close_to_close` while being scored on exec
returns. 18 tests in the file passed; 52 across the three files that import
`exec_basis_eval` passed. #78's rule again (a lexical assertion cannot hold a
semantic claim), failing in the worst direction: it waved through exactly the defect
the commit under test had just fixed. It also had a false-positive direction —
refactoring that line into a helper deletes the tokens and reddens a correct build.

The identity restatement is now the extracted `exec_identity()` and the claim is
BEHAVIOURAL: close-default config in, `view=decision` / `return_basis=exec_to_exec`
out, and every other field bit-identical.

MEDIUM-2 — the exec path declared `view=decision` unconditionally, and on two
classes of run that sentence was false. `view` is defined as "the information set
the FACTOR VALUES were taken under", but a with-book run has TWO information sets:
the subject factor's and the book's (value_ep / value_bp / volatility_20 take
close(d) and the evening daily_basic(d) — design §1.1's live defect, open until D7).
One field silently reported the clean one. That is the "report declares a check it
did not do" failure this field exists to prevent, one level up. Free to fix now
because no artifact carries it yet; after C5/C6 it would be baked into 22 files.

Chose the coordinator's (1)+(2): derive what can be derived, register what cannot.

  * `EvalConfig.book_view` (None = no book) — the book's view, declared by the ONLY
    party that knows it, the caller. `run_exec_basis_evaluation(book_view="close")`
    defaults to what every current caller actually does (the J4 rule: a default must
    be true for the callers that omit it). The no-book and with-book runs now get
    SEPARATE configs, so neither has to speak for the other. Deliberately NOT
    pairing-checked against the basis: a close-view book under an exec basis IS the
    disclosure, and refusing it would not un-mix the information sets, only stop the
    artifact from saying they are mixed.
  * `subject_view()` derives the SUBJECT's view from a measured fact —
    `factors.compute.minute.binding.NOT_DECISION_CUTOFF_SAFE`, a deny list of
    factors whose compute applies no 14:50 truncation. It lives with the factors
    (it is a fact about values, not about the eval path) and
    tests/test_decision_cutoff_visibility.py MEASURES it; the test's local copy of
    the name is gone, because two copies are two things to keep in step and the
    failure mode of drifting apart is an artifact declaring an information set its
    values do not have.
  * Consequence, intended and loud: `jump_amount_corr_20` yields
    (close, exec_to_exec), which `EvalConfig` refuses. Correct — until that factor
    is truncated there IS no honest exec artifact of it, and a blocked run beats a
    false one (red line #9). The generic pairing message is re-raised with the
    specific reason so nobody hunts for a config typo. The separate correctness-fix
    PR clears it by dropping one entry from the deny list.

LOW-1 — `test_identity_phrase_is_authored_once` asserted the phrase's FORMAT, not
that no second place spells it. The property does hold (one producer, three call
sites) but a name must not claim more than its assertions: renamed to
`test_identity_phrase_covers_both_pairings_and_states_the_book`.

A GAP I FOUND IN MY OWN NEW TESTS, and closed. Unit-testing `exec_identity` says
nothing about how the caller WIRES it: passing `book_view=None` for the with-book run
— a book was supplied, the report says there was none — passed all 28 tests. The
end-to-end assertion now lives in tests/test_exec_basis_eval.py, reads both written
JSONs, and checks the rendered Markdown says it too. Same shape as MEDIUM-1, one
level down; recorded rather than quietly patched.

Mutation evidence (every mutation first asserted to change its target):
  A  gut `exec_identity` to `replace(eval_cfg)` (the review's own escape)
     -> 3 FAIL (was: 52 pass)                                    rc=1 -> restored pass
  B  with-book run gets `book_view=None`
     -> pre-check: 28 passed (the gap, proven before fixing);
        after adding the wiring test -> 1 FAIL `assert None == 'close'`  rc=1 -> pass
  C  empty `NOT_DECISION_CUTOFF_SAFE`
     -> 3 FAIL, incl. the MEASUREMENT test finding jump dirty     rc=1 -> restored pass
  D  `subject_view` back to a hard-coded `View.DECISION`
     -> 1 FAIL                                                   rc=1 -> restored pass

Catalogue §七之二 updated BEFORE any artifact regenerates: `eval_config` is +3 keys
(not +2), the no_book/with_book blocks now legitimately differ in `book_view`, and
the jump_amount_corr exec block is registered as an intended loud refusal.

Gates: pytest 2228 passed / ruff clean / 31 configs / phase0 ic 0.9600 annual 0.8408
unchanged / provisional anchors green / exec baseline --verify 77/77 at 45c14aa /
frozen panel mtimes untouched.
@StackOverFlow11
StackOverFlow11 merged commit ebef2bb into main Jul 25, 2026
StackOverFlow11 added a commit that referenced this pull request Jul 25, 2026
docs: record refactor step D5b (PR #99) and the confirmed jump_amount_corr look-ahead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant